﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

namespace Mongoose.Games.BfgRoster
{
    /// <summary>
    /// A SimpleSet is an <see cref="ICollection{T}">ICollection</see> that contains items of type T in insertion order without allowing duplicates.
    /// </summary>
    /// <remarks>
    /// <para>The items are maintained in a sorted order (determined by insert order), and duplicate items are not allowed.</para>
    /// <para>The items are compared in one of three ways. If <typeparamref name="T"/> implements <see cref="IComparable{T}"/> or
    /// <see cref="IComparable"/>, then the <see cref="IComparable{T}.CompareTo"/> method of that interface will be used to compare items.
    /// Alternatively, an instance of <see cref="IEqualityComparer{T}"/> can be supplied.</para>
    /// <para><seealso cref="SimpleSet{T}"/> is implemented as an ordered list. Elements in this collection can be accessed using an integer
    /// index. Indexes in this collection are zero-based. <see cref="SimpleSet{T}"/> accepts <see langkeyword="null">null</see> as a valid value
    /// for reference types.</para>
    /// </remarks>
    /// <typeparam name="T">The type of values associated with the keys.</typeparam>
    /// <threadsafety static="true" instance="false" />
    [DebuggerDisplay("Count = {Count}")]
    [DebuggerTypeProxy(typeof(CollectionDebugView<>))]
    [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "A Set is a specific concept in data structures and naming it as such increases understanding")]
    public class SimpleSet<T> : Collection<T>, ICollection<T>, INotifyCollectionChanged, INotifyPropertyChanged
    {
        #region Fields

        private readonly IEqualityComparer<T> comparer;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new <see cref="SimpleSet{T}"/> using the default <see cref="IEqualityComparer{T}"/> for <typeparamref name="T"/>.
        /// </summary>
        /// <exception cref="InvalidOperationException"><typeparamref name="T"/> does not implement <see cref="IComparable{T}"/>.</exception>
        public SimpleSet()
            : this(null, null)
        {
        }

        /// <summary>
        /// Creates a new <see cref="SimpleSet{T}"/> using the default <see cref="IEqualityComparer{T}"/> for <typeparamref name="T"/> and the supplied initial items.
        /// </summary>
        /// <param name="sequence">The sequence whose elements are copied to the new <see cref="SimpleSet{T}"/>.</param>
        /// <remarks>Duplicate items supplied in <paramref name="sequence"/> will be filtered.</remarks>
        /// <exception cref="InvalidOperationException"><typeparamref name="T"/> does not implement <see cref="IComparable{T}"/>.</exception>
        public SimpleSet(IEnumerable<T> sequence)
            : this(null, sequence)
        {
        }

        /// <summary>
        /// Creates a new <see cref="SimpleSet{T}"/> using the supplied <see cref="IEqualityComparer{T}"/>.
        /// </summary>
        /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> implementation to use when comparing values, or <see langkeyword="null">null</see> to use the default <see cref="EqualityComparer{T}"/> for the type of the items in the list.</param>
        /// <exception cref="InvalidOperationException"><typeparamref name="T"/> does not implement <see cref="IComparable{T}"/>.</exception>
        public SimpleSet(IEqualityComparer<T> comparer)
            : this(comparer, null)
        {
        }

        /// <summary>
        /// Creates a new <see cref="SimpleSet{T}"/> using the supplied <see cref="IEqualityComparer{T}"/> and the supplied initial items.
        /// </summary>
        /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> implementation to use when comparing values, or <see langkeyword="null">null</see> to use the default <see cref="EqualityComparer{T}"/> for the type of the items in the list.</param>
        /// <param name="sequence">The sequence whose elements are copied to the new <see cref="SimpleSet{T}"/>.</param>
        /// <remarks>Duplicate items supplied in <paramref name="sequence"/> will be filtered.</remarks>
        /// <exception cref="InvalidOperationException"><typeparamref name="T"/> does not implement <see cref="IComparable{T}"/>.</exception>
        public SimpleSet(IEqualityComparer<T> comparer, IEnumerable<T> sequence)
        {
            this.comparer = comparer ?? EqualityComparer<T>.Default;
            if (sequence == null)
                return;

            foreach (var item in sequence)
            {
                this.Add(item);
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Removes all elements from the <see cref="SimpleSet{T}"/>.
        /// </summary>
        /// <remarks>
        /// <para><see cref="ICollection{T}.Count"/> is set to zero, and references to other objects from elements of the collection are also released.</para>
        /// <para>This method is an O(n) operation, where n is <seealso cref="ICollection{T}.Count"/>.</para>
        /// <para><note type="inheritinfo">Derived classes can override this method to change the behavior of the <seealso cref="Collection{T}.Clear"/> method.</note></para>
        /// <para>
        /// This method will raise the following events in order:
        /// <list type="table">
        ///     <listheader>
        ///         <term>Event</term>
        ///         <description>Description</description>
        ///     </listheader>
        ///     <item>
        ///         <term><see cref="PropertyChanged"/></term>
        ///         <description>The <see cref="PropertyChangedEventArgs.PropertyName"/> value as "Count"</description>
        ///     </item>
        ///     <item>
        ///         <term><see cref="PropertyChanged"/></term>
        ///         <description>The <see cref="PropertyChangedEventArgs.PropertyName"/> value as "Item[]"</description>
        ///     </item>
        ///     <item>
        ///         <term><see cref="CollectionChanged"/></term>
        ///         <description>The <see cref="NotifyCollectionChangedAction"/> value as "Reset"</description>
        ///     </item>
        /// </list>
        /// </para>
        /// </remarks>
        protected override void ClearItems()
        {
            base.ClearItems();

            this.OnPropertyChanged("Count");
            this.OnPropertyChanged("Item[]");
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        /// <summary>
        /// Determines if this set contains an item equal to <paramref name="value"/>. The set is not changed.
        /// </summary>
        /// <remarks>
        /// <para>This method determines equality using the configured equality comparer for <typeparamref name="T"/>, the type of values in the list.</para>
        /// <para>This method performs a linear search; therefore, the average execution time is proportional to <see cref="ICollection{T}.Count"/>. That is, 
        /// this method is an <i>O(n)</i> operation, where <i>n</i> is <seealso cref="ICollection{T}.Count"/>.</para>
        /// </remarks>
        /// <param name="value">The item to search for.</param>
        /// <returns><see langkeyword="true">True</see> if the set contains <paramref name="value"/>; otherwise <see langkeyword="false">false</see>.</returns>
        public virtual new Boolean Contains(T value)
        {
            return this.Any(item => this.comparer.Equals(item, value));
        }

        Boolean ICollection<T>.Contains(T value)
        {
            return this.Contains(value);
        }

        /// <summary>
        /// Inserts an element into the <see cref="SimpleSet{T}"/> at the specified index.
        /// </summary>
        /// <remarks>
        /// This method will raise the following events in order:
        /// <list type="table">
        ///     <listheader>
        ///         <term>Event</term>
        ///         <description>Description</description>
        ///     </listheader>
        ///     <item>
        ///         <term><see cref="PropertyChanged"/></term>
        ///         <description>The <see cref="PropertyChangedEventArgs.PropertyName"/> value as "Count"</description>
        ///     </item>
        ///     <item>
        ///         <term><see cref="PropertyChanged"/></term>
        ///         <description>The <see cref="PropertyChangedEventArgs.PropertyName"/> value as "Item[]"</description>
        ///     </item>
        ///     <item>
        ///         <term><see cref="CollectionChanged"/></term>
        ///         <description>The <see cref="NotifyCollectionChangedAction"/> value as "Add"</description>
        ///     </item>
        /// </list>
        /// </remarks>
        /// <param name="index">The zero-based index at which <paramref name="item"/> should be inserted.</param>
        /// <param name="item">The object to insert. The value can be <see langkeyword="null">null</see> for reference types.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <para><paramref name="index"/> is less than zero</para>
        /// <para>-OR-</para>
        /// <para><paramref name="index"/> is greater than <see cref="ICollection{T}.Count"/></para>
        /// </exception>
        protected override void InsertItem(Int32 index, T item)
        {
            if (this.Contains(item))
                return;

            base.InsertItem(index, item);

            this.OnPropertyChanged("Count");
            this.OnPropertyChanged("Item[]");
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        }

        Boolean ICollection<T>.Remove(T value)
        {
            return this.Remove(value);
        }

        /// <summary>
        /// Searches for the specified <paramref name="value"/> and returns the zero-based index of the first occurrence within the entire <see cref="SimpleSet{T}"/>.
        /// </summary>
        /// <remarks>
        /// <para>The <see cref="SimpleSet{T}"/> is searched forward starting at the first element and ending at the last element.</para>
        /// <para>This method determines equality using the configured <see cref="IEqualityComparer{T}">equality comparer</see>.</para>
        /// <para>This method performs a linear search; therefore, the average execution time is proportional to <see cref="ICollection{T}.Count"/>. That is, this method is an <i>O(n)</i> operation, where <i>n</i> is <seealso cref="ICollection{T}.Count"/>.</para>
        /// </remarks>
        /// <param name="value">The object to locate in the <see cref="SimpleSet{T}"/>. The value can be <see langkeyword="null">null</see> for reference types.</param>
        /// <returns>The zero-based index of the first occurrence of item within the entire <see cref="SimpleSet{T}"/>, if found; otherwise, -1.</returns>
        public new Int32 IndexOf(T value)
        {
            return this.FindIndex(0, this.Count, item => this.comparer.Equals(item, value));
        }

        /// <summary>
        /// Removes the first occurrence of a specific value from the <see cref="SimpleSet{T}"/>.
        /// </summary>
        /// <remarks>
        /// This method will raise the following events in order:
        /// <list type="table">
        ///     <listheader>
        ///         <term>Event</term>
        ///         <description>Description</description>
        ///     </listheader>
        ///     <item>
        ///         <term><see cref="PropertyChanged"/></term>
        ///         <description>The <see cref="PropertyChangedEventArgs.PropertyName"/> value as "Count"</description>
        ///     </item>
        ///     <item>
        ///         <term><see cref="PropertyChanged"/></term>
        ///         <description>The <see cref="PropertyChangedEventArgs.PropertyName"/> value as "Item[]"</description>
        ///     </item>
        ///     <item>
        ///         <term><see cref="CollectionChanged"/></term>
        ///         <description>The <see cref="NotifyCollectionChangedAction"/> value as "Remove"</description>
        ///     </item>
        /// </list>
        /// </remarks>
        /// <param name="value">The value to remove from the <see cref="SimpleSet{T}"/>. The value can be <see langkeyword="null">null</see>.</param>
        /// <returns>
        /// <see langkeyword="true">true</see> if the <paramref name="value"/> is successfully removed; otherwise, <see langkeyword="false">false</see>.
        /// This method also returns <see langkeyword="false">false</see> if item was not found in the original <see cref="SimpleSet{T}"/>.
        /// </returns>
        public virtual new Boolean Remove(T value)
        {
            var index = this.IndexOf(value, this.comparer);
            if (index < 0)
                return false;

            this.RemoveAt(index);

            this.OnPropertyChanged("Count");
            this.OnPropertyChanged("Item[]");
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, value, -1));

            return true;
        }

        /// <summary>
        /// Replaces the element at the specified index.
        /// </summary>
        /// <remarks>
        /// This method will raise the following events in order:
        /// <list type="table">
        ///     <listheader>
        ///         <term>Event</term>
        ///         <description>Description</description>
        ///     </listheader>
        ///     <item>
        ///         <term><see cref="CollectionChanged"/></term>
        ///         <description>The <see cref="NotifyCollectionChangedAction"/> value as "Replace"</description>
        ///     </item>
        /// </list>
        /// </remarks>
        /// <param name="index">The zero-based index of the element to replace.</param>
        /// <param name="item">The new value for the element at the specified index. The value can be <see langkeyword="null">null</see>.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <para>The parameter <paramref name="index"/> is less than zero</para>
        /// <para>-OR-</para>
        /// <para><paramref name="index"/> is greater than <see cref="ICollection{T}.Count"/>.</para>
        /// </exception>
        protected override void SetItem(Int32 index, T item)
        {
            if (this.Contains(item))
                return;

            var oldItem = this[index];
            base.SetItem(index, item);

            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem, index));
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        /// <remarks>The <seealso cref="PropertyChanged"/> event can indicate all properties on the object have changed by using either <see langkeyword="null">null</see> or <see cref="String.Empty"/> as the property name in the <see cref="PropertyChangedEventArgs"/>.</remarks>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="propertyName">The name of the property that changed.</param>
        protected virtual void OnPropertyChanged(String propertyName)
        {
            if (this.PropertyChanged == null)
                return;

            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region INotifyCollectionChanged Members

        /// <summary>
        /// Occurs when the collection has changed.
        /// </summary>
        /// <remarks>The event handler receives an argument of type <see cref="NotifyCollectionChangedEventArgs"/>, which contains data that is related to this event.</remarks>
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        /// <summary>
        /// Raises the <see cref="CollectionChanged"/> event.
        /// </summary>
        /// <param name="args">The <see cref="NotifyCollectionChangedEventArgs"/> event data.</param>
        /// <exception cref="ArgumentNullException"><paramref name="args"/> is <see langkeyword="null">null</see>.</exception>
        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
        {
            if (args == null)
                throw new ArgumentNullException("args");
            if (this.CollectionChanged == null)
                return;

            this.CollectionChanged(this, args);
        }

        #endregion
    }
}